<!DOCTYPE html>
<html>
  <head>
    <!-- [[! Document Settings ]] -->
    <meta charset="utf-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />

    <!-- [[! Page Meta ]] -->
    <title>[译] Android 架构：Part 4 —— 实践 Clean Architecture（含源码）</title>
    <meta name="description" content="在编码 - " />

    <meta name="HandheldFriendly" content="True" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />

    <link rel="shortcut icon" href="/assets/images/favicon.ico" >

    <!-- [[! Styles'n'Scripts ]] -->
    <link rel="stylesheet" type="text/css" href="/assets/css/screen.css" />
    <link rel="stylesheet" type="text/css"
      href="//fonts.googleapis.com/css?family=Merriweather:300,700,700italic,300italic|Open+Sans:700,400" />

    <link rel="stylesheet" type="text/css" href="/assets/css/prism.css" />

    <!-- [[! Ghost outputs important style and meta data with this tag ]] -->
    <link rel="canonical" href="/" />
<meta name="referrer" content="origin" />
<link rel="next" href="/page2/" />

<meta property="og:site_name" content="在编码" />
<meta property="og:type" content="website" />
<meta property="og:title" content="在编码" />
<meta property="og:description" content="" />
<meta property="og:url" content="/" />
<meta property="og:image" content="/assets/images/cover1.jpg" />

<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="在编码" />
<meta name="twitter:description" content="" />
<meta name="twitter:url" content="/" />
<meta name="twitter:image:src" content="/assets/images/cover1.jpg" />

<script type="application/ld+json">
  {
  "@context": "http://schema.org",
  "@type": "Website",
  "publisher": "Finding The Way Home",
  "url": "/",
  "image": "/assets/images/cover1.jpg",
  "description": ""
  }
</script>

<meta name="generator" content="Jekyll 3.0.0" />
<link rel="alternate" type="application/rss+xml" title="在编码" href="/feed.xml" />


  </head>
  <body class="home-template nav-closed">

    <div class="nav">
  <h3 class="nav-title">Home</h3>
  <a href="#" class="nav-close">
    <span class="hidden">Close</span>
  </a>
  <ul>
    <li class="nav-home " role="presentation"><a href="/">Home</a></li>
    <!-- <li class="nav-about " role="presentation"><a href="/about">About</a></li>
    <li class="nav-author " role="presentation"><a href="/author/casper">Author</a></li> -->
    <li class="nav-ios " role="presentation"><a href="/tag/iOS">iOS</a></li>
    <li class="nav-mvc " role="presentation"><a href="/tag/Android">Android</a></li>
    <li class="nav-server " role="presentation"><a href="/tag/Server">Server</a></li>
  </ul>
  <a class="subscribe-button icon-feed" href="/feed.xml">Subscribe</a>
</div>
<span class="nav-cover"></span>


    <div class="site-wrapper">

      <!-- [[! Everything else gets inserted here ]] -->
      <!-- < default -->

<!-- The comment above "< default" means - insert everything in this file into -->
<!-- the [body] of the default.hbs template, which contains our header/footer. -->

<!-- Everything inside the #post tags pulls data from the post -->
<!-- #post -->

<header class="main-header post-head no-cover">
  <nav class="main-nav overlay clearfix">
      <a class="back-button icon-arrow-left" href='/'>Home</a>
    <a class="menu-button icon-menu" href="#"><span class="word">Menu</span></a>
  </nav>
</header>

<main class="content" role="main">

  <article class="post">

    <header class="post-header">
      <h1 class="post-title">[译] Android 架构：Part 4 —— 实践 Clean Architecture（含源码）</h1>
      <section class="post-meta">
        <a href='/'>Listen</a>
        <time class="post-date" datetime="2017-07-27">27 Jul 2017</time>
        <!-- [[tags prefix=" on "]] -->
        
        on
        
        
        <a href='/tag/Android'>Android</a>,
        
        
        
        <a href='/tag/Clean Architecture'>Clean Architecture</a>
        
        
        
      </section>
    </header>

    <section class="post-content">

      <p><img src="/images/android/main_image.png" alt=""></p>

<p>在 <a href="./android-architecture-part-3-applying-clean-architecture-android">Android 架构系列的最后部分</a>，我们将 Clean Architecture 调整到 Android 平台。我们将 Android 和真实世界从业务逻辑中分离，令利益相关者满意，使一切都容易测试。</p>

<p>理论很棒，但是当我们创建一个新 Android 项目时，该从哪开始呢？让我们用整洁代码弄脏我们的手，把空白画布变成一个架构。</p>

<h2>基础</h2>

<p>让我们先做一些基础工作 —— 创建模块并建立依赖关系，使其与依赖规则保持一致。</p>

<p>这些将是我们的模块，从抽象到具体：</p>

<h3>1. domain</h3>

<p>实体、用例、仓库接口和设备接口放入 domain 模块。</p>

<p>理想情况下，实体和业务逻辑应该是平台无关的。为了安全起见，为了防止我们在这里放置一些 Android 的东西，我们将使它成为一个纯 java 模块。</p>

<h3>2. data</h3>

<p>data 模块应当持有与数据持久化和操作相关的所有内容。在这里我们可以找到 DAO、ORM、SharedPreferences、网络相关的比如 Retrofit Service 或类似的东西。</p>

<h3>3. device</h3>

<p>device 模块应该拥有与 Android 相关的所有东西（除了数据持久化 和 UI）。例如 ConnectivityManager, NotificationManager 和 misc 传感器的封装类。</p>

<p>我们将使 data 和 device 模块成为 Android 模块，因为它们必须知道 Android，不能是纯的 java。</p>

<h3>4. 最容易的部分，app 模块（UI 模块）</h3>

<p>当你创建项目时，该模块已经由 Android Studio 为你创建好了。</p>

<p>在这里，你可以放置与 Android UI 相关的类，譬如 Presenter，Controller，ViewModel，Adapter 以及 View。</p>

<h2>依赖</h2>

<p>依赖规则定义了具体模块依赖于抽象模块。</p>

<p>你可能会记得本系列的第三部分，UI（app），DB-API（data）以及设备（device）的东西都在外层。这意味着它们在同一抽象级别。那么我们该如何将它们串在一起呢？</p>

<p>理想的情况下，这些模块仅依赖于领域（domain）模块。在这种情况下，依赖关系看起来有点像一颗星：</p>

<p><img src="../images/android/graf_2_A-1.png" alt=""></p>

<p>但是这里我们涉及 Android 所以事情就不那么完美了。因为我们需要创建对象依赖图以及初始化一些东西，模块有时依赖另一个模块而不是领域模块。</p>

<p>例如，我们需要在 app 模块创建依赖注入的对象依赖图，这就迫使 app 模块需要知道其余所有模块。</p>

<p>我们调整后的依赖关系图：</p>

<p><img src="../images/android/graf_2_B-1.png" alt=""></p>

<h2>砖，更多的砖</h2>

<p>终于，是时候写些代码了。为了容易演示，我们将以 RSS 阅读器 app 为例。我们的用户应该能够管理他们的 RSS Feed 订阅，从 Feed 中获取文章并阅读它们。</p>

<h2>领域</h2>

<p>让我们从领域层开始，创建我们的核心业务模型和逻辑。</p>

<p>我们的业务模型非常简单：</p>

<ul>
<li><em>Feed</em> - 持有 RSS Feed 相关数据比如 url、缩略 URL、标题和描述</li>
<li><em>Article</em> - 持有文章相关数据比如文章标题、url 和发表时间</li>
</ul>

<p>至于我们的逻辑，我们将使用 UseCase 也就是交互器。它们将简单类中的小部分业务逻辑封装起来。它们都会实现一般的 UseCase 协议：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">public interface UseCase&lt;P&gt; {

   interface Callback {

      void onSuccess();
      void onError(Throwable throwable);
    }

   void execute(P parameter, Callback callback);
 }
</code></pre></figure>
<p>当用户打开我们的 app 要做的第一件事情是添加一个新的 RSS Feed 订阅。所以从我们的交互器开始，我们创建 <code>AddNewFeedUseCase</code>，以及处理 feed 添加和验证逻辑的辅助类。</p>

<p><code>AddNewFeedUseCase</code> 使用 <code>FeedValidator</code> 来检查 feed URL 的有效性，我们还将创建 <code>FeedRepository</code> 协议，它为我们的业务逻辑提供基础的增删改查能力来管理 feed 数据：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">public interface FeedRepository {

    int createNewFeed(String feedUrl);

    List&lt;Feed&gt; getUserFeeds();

    List&lt;Article&gt; getFeedArticles(int feedId);

    boolean deleteFeed(int feedId);
}
</code></pre></figure>
<p>注意我们在领域层的<strong>命名</strong>是如何清晰地说明我们的 app 是做什么的。</p>

<p>把所有东西放在一起，我们的 <code>AddNewFeedUseCase</code> 看起来像这样：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">public final class AddNewFeedUseCase implements UseCase&lt;String&gt; {

   private final FeedValidator feedValidator;
   private final FeedRepository feedRepository;

   @Override
   public void execute(final String feedUrl, final Callback callback) {
       if (feedValidator.isValid(feedUrl)) {
           onValidFeedUrl(feedUrl, callback);
       } else {
           callback.onError(new InvalidFeedUrlException());
       }
   }

   private void onValidFeedUrl(final String feedUrl, final Callback callback) {
       try {
           feedRepository.createNewFeed(feedUrl);
           callback.onSuccess();
       } catch (final Throwable throwable) {
           callback.onError(throwable);
       }
   }
}

</code></pre></figure>
<p>为了简洁起见，省略了构造函数。</p>

<p>现在，你可能会困惑，为什么我们的用例和回调是一个接口？</p>

<p>为了更好地演示我们下一个问题，让我们来研究研究 <code>GetFeedArticlesUseCase</code></p>

<p>获得 feedId -&gt; 通过 FeedRepository 抓取 feed 文章 -&gt; 返回 feed 文章列表</p>

<p>这是数据流问题，用例位于表现层和数据层之间，我们怎样建立起层和层之间的通信？记得那些输入和输出端口吗？</p>

<p><img src="../images/android/graf_1_b.png" alt=""></p>

<p>我们的用例必须实现输入端口（interface）。Presenter 调用用例的方法，数据流向用例（feedId）。用例将 feedId 转换成 feed 文章列表，并希望将其返回给表现层。它拥有指向输出端口（Callback）的引用，因为输出端口是定义在同一层的，所以它调用了输出端口的一个方法。因此数据将发送到输出端口 —— Presenter。</p>

<p>我们稍微调整一下 UseCase 协议：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">public interface UseCase&lt;P, R&gt; {

   interface Callback&lt;R&gt; {
       void onSuccess(R return);
       void onError(Throwable throwable);
   }

   void execute(P parameter, Callback&lt;R&gt; callback);
}

public interface CompletableUseCase&lt;P&gt; {

   interface Callback {
       void onSuccess();
       void onError(Throwable throwable);
   }

   void execute(P parameter, Callback callback);
}
</code></pre></figure>
<p><em>UseCase</em> 接口是输入端口，而 <em>Callback</em> 接口是输出端口。</p>

<p><code>GetFeedArticlesUseCase</code> 实现如下：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">class GetFeedArticlesUseCase implements UseCase&lt;Integer, List&lt;Article&gt;&gt; {

   private final FeedRepository feedRepository;

   @Override
   public void execute(final Integer feedId, final Callback&lt;List&lt;Article&gt;&gt; callback) {
       try {
           callback.onSuccess(feedRepository.getFeedArticles(feedId));
       } catch (final Throwable throwable) {
           callback.onError(throwable);
       }
   }
 }

</code></pre></figure>
<p>最后一件领域层需要注意的事情是，交互器只应该包含业务逻辑。在这样做时，它们可以使用 Repository，组合其它交互器，使用类似我们例子中 <code>FeedValidator</code> 这样的公共设施类。</p>

<h2>UI</h2>

<p>很好，我们可以抓取文章，现在让我们向用户展示它们。</p>

<p>我们的 View 有一个简单的协议：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">interface View {

   void showArticles(List&lt;ArticleViewModel&gt; feedArticles);

   void showErrorMessage();

   void showLoadingIndicator();
}
</code></pre></figure>
<p>此 View 的 Presenter 的表现逻辑非常简单。它抓取文章，转换成视图模型传递给 View，简单吧，对吗？</p>

<p>简单的 Presenter 是 Clean Architecture 和 表现 —— 业务逻辑分离的另一个伟大成就。</p>

<p>这是我们的 <code>FeedArticlesPresenter</code>：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">class FeedArticlesPresenter implements UseCase.Callback&lt;List&lt;Article&gt;&gt; {

   private final GetFeedArticlesUseCase getFeedArticlesUseCase;
   private final ViewModeMapper viewModelMapper;

   public void fetchFeedItems(final int feedId) {
       getFeedArticlesUseCase.execute(feedId, this);
   }

   @Override
   public void onSuccess(final List&lt;Article&gt; articles) {
       getView().showArticles(viewModelMapper.mapArticlesToViewModels(articles));
   }

   @Override
   public void onError(final Throwable throwable) {
       getView().showErrorMessage();
   }
 }

</code></pre></figure>
<p>注意 <code>FeedArticlesPresenter</code> 实现了 Callback 接口，并将自身传递给用例，它实际上是用例的输出端口，并以这种方式关闭数据流。这是我们前面提到过的数据流的具体例子，我们可以在流程图上调整标签来匹配这个例子：</p>

<p><img src="../images/android/graf_1.png" alt=""></p>

<p>我们的参数 P 是整数 feedId，返回类型 R 是文章列表。</p>

<p>你不一定必须使用 Presenter 来处理表现逻辑，我们可以说，Clean Architecture 是“前端”无关的 —— 这意味着你可以使用 MVP，MVC，MVVM 或其他任何东西。</p>

<h2>我们来加点 Rx</h2>

<p>如果你想知道为什么会有这样关于 RxJava 的炒作，那么来看看我们的用例的响应式实现：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">public interface UseCase&lt;P, R&gt; {

   Single&lt;R&gt; execute(P parameter);
}

public interface CompletableUseCase&lt;P&gt; {

   Completable execute(P parameter);
}
</code></pre></figure>
<p>回调接口现在已经消失，我们使用 RxJava Single / Completable 接口作为输出端口。</p>

<p>响应式 <code>FeedArticlePresenter</code> 的实现如下：</p>
<figure class="highlight"><pre><code class="language-java" data-lang="java">class FeedArticlesPresenter {

   private final GetFeedArticlesUseCase getFeedArticlesUseCase;
   private final ViewModeMapper viewModelMapper;

   public void fetchFeedItems(final int feedId) {
       getFeedItemsUseCase.execute(feedId)
                  .map(feedViewModeMapper::mapFeedItemsToViewModels)
                  .subscribeOn(Schedulers.io())
                  .observeOn(AndroidSchedulers.mainThread())
                  .subscribe(this::onSuccess, this::onError);
   }

   private void onSuccess(final List articleViewModels) {
      getView().showArticles(articleViewModels);
   }

   private void onError(final Throwable throwable) {
      getView().showErrorMessage();
   }
}
</code></pre></figure>
<p>虽然有点隐蔽，相同的数据流反转原则仍然存在，因为没有 RxJava，Presenter 会实现回调，而使用 RxJava， 订阅者也包含在外层 —— Presenter 的某个地方。</p>

<blockquote>
<p>译者注：如果你打算用 ViewModel 取代 Presenter，并且在项目中使用了 RxJava，那么向你安利 <a href="./implement-mvvm-on-android-using-rxcommand">使用 RxCommand 在 Android 上实现 MVVM</a>。</p>
</blockquote>

<h2>Data and Device</h2>

<p>data 和 device 模块包含所有业务逻辑不关心的实现细节。它只关系协议，使你容易测试，以及在不触及业务逻辑的情况下更换实现。</p>

<p>这里，你可以使用你喜欢的 ORM 或 DAO 来存储本地数据，使用你喜欢的网络服务来从网络获取数据。我们将实现 <code>FeedService</code> 来拉取文章，使用 <code>FeedDao</code> 来存储文章数据到设备。</p>

<p>每个数据源（网络和本地存储）都有自己的模型来处理。</p>

<p>在我们的例子中，它们是 ApiFeed - ApiArticle 和 DbFeed - DbArticle。</p>

<p><code>FeedRepository</code> 的具体实现也可以在 data 模块中找到。</p>

<p>device 模块持有 Notifications 协议的实现，就是对 NotificationManager 类的一个包装。当有新的用户可能感兴趣并参与的文章发表时，我们会在我们的业务逻辑中使用 Notifications 来向用户显示一个通知。</p>

<h2>模型，到处都是模型</h2>

<p>你可能已经注意到，我们提及的模型不仅仅是实体或业务。</p>

<p>实际上，我们还有 db 模型，API 模型，视图模型，当然还有业务模型。</p>

<p>每一层都有自己的模型是个不错的实践，这样你的具体细节，譬如视图，就不需要依赖低层实现的具体细节。通过这种方式，举个例子，当你决定更换 ORM 框架时，你就不需要破坏不相干的代码。</p>

<p>为了确保这点，有必要在每个层中使用对象映射器。在我们的例子中，我们使用 <code>ViewModelMapper</code> 将 demain 模块中的 Article 模型映射成 <code>ArticleViewModel</code></p>

<h2>总结</h2>

<p>遵循这些准则，我们创建了健壮且通用的架构。首先，看起要写好多代码，确实也是，但是记住，我们是为未来的变化和功能搭建我们的架构。如果你正确地做了，未来你会感谢当初的决定。</p>

<p>在下一部分中，我们会介绍可能是这个架构中最重要的部分，它的可测试性以及如何测试它。那么，在此期间，你对架构实现的哪部分最感兴趣呢？</p>

<p><a href="./android-architecture-part-1-every-new-beginning-is-hard.html">Part I</a></p>

<p><a href="./android-architecture-part-2-clean-architecture.html">Part II</a></p>

<p><a href="./android-architecture-part-3-applying-clean-architecture-android.html">part III</a></p>

<p><a href="https://github.com/fiveagency/Reedly">源码</a></p>

<p><a href="http://five.agency/android-architecture-part-4-applying-clean-architecture-on-android-hands-on/">原文</a></p>


    </section>

    <footer class="post-footer">

      <!-- Everything inside the #author tags pulls data from the author -->
      <!-- #author-->

      
      <figure class="author-image">
        <a class="img" href='/' style="background-image: url(/assets/images/me.jpg)"><span class="hidden">'s Picture</span></a>
      </figure>
      

      <section class="author">
        <h4><a href='/'>Listen</a></h4>

        
        <p> 移动开发工程师，熟悉 Android、iOS、ReactNative</p>
        
        <div class="author-meta">
          <span class="author-location icon-location"> 深圳</span>
          <span class="author-link icon-link"><a href="https://github.com/listenzz"> Github</a></span>
        </div>
      </section>

      <!-- /author  -->

      <section class="share">
        <h4>Share this post</h4>
        <a class="icon-twitter" href="http://twitter.com/share?text=[译] Android 架构：Part 4 —— 实践 Clean Architecture（含源码）&amp;url=https://github.com/listenzzandroid-architecture-part-4-applying-clean-architecture-on-android-hands-on.html"
          onclick="window.open(this.href, 'twitter-share', 'width=550,height=235');return false;">
          <span class="hidden">Twitter</span>
        </a>
        <a class="icon-facebook" href="https://www.facebook.com/sharer/sharer.php?u=https://github.com/listenzzandroid-architecture-part-4-applying-clean-architecture-on-android-hands-on.html"
          onclick="window.open(this.href, 'facebook-share','width=580,height=296');return false;">
          <span class="hidden">Facebook</span>
        </a>
        <a class="icon-google-plus" href="https://plus.google.com/share?url=https://github.com/listenzzandroid-architecture-part-4-applying-clean-architecture-on-android-hands-on.html"
          onclick="window.open(this.href, 'google-plus-share', 'width=490,height=530');return false;">
          <span class="hidden">Google+</span>
        </a>
      </section>

      <!-- Add Disqus Comments -->
      <!-- <section class="disqus">
    <div id="disqus_thread"></div>
    <script type="text/javascript">
        var disqus_shortname  = 'listenzz';
        var disqus_identifier = '[译] Android 架构：Part 4 —— 实践 Clean Architecture（含源码）';
        var disqus_title      = "[译] Android 架构：Part 4 —— 实践 Clean Architecture（含源码）";
        
        (function() {
         var dsq = document.createElement('script'); dsq.type = 'text/javascript'; dsq.async = true;
         dsq.src = '//' + disqus_shortname + '.disqus.com/embed.js';
         (document.getElementsByTagName('head')[0] || document.getElementsByTagName('body')[0]).appendChild(dsq);
         })();
        </script>
    <noscript>Please enable JavaScript to view the <a href="https://disqus.com/?ref_noscript">comments powered by Disqus.</a></noscript>
    <a href="http://disqus.com" class="dsq-brlink">blog comments powered by <span class="logo-disqus">Disqus</span></a>
</section>
 -->

    </footer>

  </article>

</main>

<aside class="read-next">

  <!-- [[! next_post ]] -->
  
  <a class="read-next-story no-cover" href="/android-lifecyle-works-perfectly-with-rxjava.html">
    <section class="post">
      <h2>Android 生命周期架构组件与 RxJava 完美协作</h2>
      <p>本文主要讲述了 Android 生命周期架构组件如何与 RxJava 协作，从此过上了幸福生活的故事。 涉及的内容有： - Android 生命周期架构组件 - LiveData 有哪些惊艳特性 - 如何使用 RxJava...</p>
    </section>
  </a>
  
  <!-- [[! /next_post ]] -->
  <!-- [[! prev_post ]] -->
  
  <a class="read-next-story prev no-cover" href="/android-architecture-part-3-applying-clean-architecture-android.html">
    <section class="post">
      <h2>[译] Android 架构：Part 3 —— 应用 Clean Architecture</h2>
      <p>到目前为止，在这个系列中，我们已经讲解了一些 初学者易犯的错误，以及过了一遍 Clean Architecture。在这最后一部分，我们会介绍最后一个难题：标签，或者准确地说，组件。 译者注：看完了这一部分，还有第四部分。在第四部分将会提供一个很酷的示范项目。 首先，我会移除在 Android 项目中不使用的东西，添加一些在 Uncle Bob 的图中找不到的但我们需要使用的东西，看起来像这样： 我会从中心（抽象）讲到边缘（具体）。 Entities 实体（又称领域对象或业务对象）是 app...</p>
    </section>
  </a>
  
  <!-- [[! /prev_post ]] -->
</aside>

<!-- /post -->


      <footer class="site-footer clearfix">
        <section class="copyright"><a href="/">在编码</a> &copy; 2020</section>
        <section class="poweredby">Proudly published with <a href="https://jekyllrb.com/">Jekyll</a> using <a href="https://github.com/biomadeira/jasper">Jasper</a></section>
      </footer>
    </div>
    <!-- [[! Ghost outputs important scripts and data with this tag ]] -->
    <script type="text/javascript" src="https://code.jquery.com/jquery-1.11.3.min.js"></script>
    <!-- [[! The main JavaScript file for Casper ]] -->
    <script type="text/javascript" src="/assets/js/jquery.fitvids.js"></script>
    <script type="text/javascript" src="/assets/js/index.js"></script>
    <script type="text/javascript" src="/assets/js/prism.js"></script>

    <!-- Add Google Analytics  -->
    <!-- Google Analytics Tracking code -->
<script>
 (function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
   (i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
   m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
 })(window,document,'script','//www.google-analytics.com/analytics.js','ga');

 ga('create', 'UA-96120107-1', 'auto');
 ga('send', 'pageview');

</script>

  </body>
</html>
